/*
 * Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license
 * Click nbfs://nbhost/SystemFileSystem/Templates/javafx/FXMLController.java to edit this template
 */
package org.flower.httprc.controller;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.google.gson.JsonParser;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Stream;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.ChoiceBox;
import javafx.scene.control.Label;
import javafx.scene.control.ListView;
import javafx.scene.control.MultipleSelectionModel;
import javafx.scene.control.ProgressBar;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.TabPane;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
import javafx.scene.control.ToggleButton;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.AnchorPane;
import javafx.stage.FileChooser;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.flower.httprc.control.JSONTree;
import org.flower.httprc.control.NotifyBox;
import org.flower.httprc.http.AccessType;
import org.flower.httprc.http.Charset;
import org.flower.httprc.http.HttpMethod;
import org.flower.httprc.http.HttpRClientX;
import org.flower.httprc.http.HttpReq;
import org.flower.httprc.http.HttpResp;
import org.flower.httprc.http.Parameter;
import org.flower.httprc.util.JavaUtil;
import org.flower.httprc.util.JsonUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * FXML Controller class
 *
 * @author ZhuYanXin
 */
public class HttpRCController implements Initializable {
    private static final Logger LOG = LoggerFactory.getLogger(HttpRCController.class);
    @FXML
    private Button newBtn;
    @FXML
    private ListView<String> reqList;
    @FXML
    private TextField filterTxt;
    // send
    @FXML
    private ChoiceBox<String> methodChB;
    @FXML
    private TextField urlTxt;
    @FXML
    private Button sendBtn;
    @FXML
    private Button saveBtn;
    @FXML
    private TabPane reqTab;
    // header
    @FXML
    private TableView<Parameter> headerReq;
    @FXML
    private TableColumn<Parameter, String> colHeaderKey;
    @FXML
    private TableColumn<Parameter, String> colHeaderVal;
    @FXML
    private TableColumn<Parameter, String> colHeaderDesc;
    @FXML
    private TableColumn<Parameter, String> colHeaderDel;
    // param
    @FXML
    private ChoiceBox<String> accessTypeChb;
    @FXML
    private ChoiceBox<String> charsetChb;
    @FXML
    private AnchorPane formParamPane;
    @FXML
    private TextArea jsonParamTxa;
    @FXML
    private TextField filesKeyTxt;
    @FXML
    private Button selectFileBtn;
    @FXML
    private ListView<String> fileList;
    @FXML
    private TableView<Parameter> formParam;
    @FXML
    private TableColumn<Parameter, String> colParamKey;
    @FXML
    private TableColumn<Parameter, String> colParamVal;
    @FXML
    private TableColumn<Parameter, String> colParamDesc;
    @FXML
    private TableColumn<Parameter, String> colParamDel;
    // response
    @FXML
    private TextArea jsonRespTxa;
    @FXML
    private ToggleButton formatRawBtn;
    @FXML
    private Button copyRespBtn;
    @FXML
    private TextArea headerResp;
    @FXML
    private TreeView<String> jsonView;
    // status
    @FXML
    private ProgressBar pg;
    @FXML
    private Label descLbl;

    private Map<String, File> mapFile;
    private HttpReq httpReq;
    private Map<String, HttpReq> mapReq;
    private final String HIS_FILE = "history" + File.separatorChar + "saved_request.json";

    @Override
    public void initialize(URL url, ResourceBundle rb) {
        reqTab.getSelectionModel().select(1);
        initChBoxValue();
        initHeaderTable();
        initParamTable();
        initAndRegEvent();
        listenerMethod();
        listenerAccessType();
        mapReq = new HashMap<>();
        loadHistory();
    }

    @FXML
    void saveReq(ActionEvent event) {
        LOG.info("saveReq:{}", httpReq);
        if (httpReq == null) {
            return;
        }
        String uri = httpReq.getUri();
        mapReq.put(uri, httpReq);
        ObservableList<String> items = reqList.getItems();
        if (!items.contains(uri)) {
            items.add(uri);
        }
        saveToFile();
    }

    private void saveToFile() {
        ObjectMapper mapper = new ObjectMapper();
        String path = JavaUtil.loadUrl("").getPath();
        File file = FileUtils.getFile(path + HIS_FILE);
        if (!file.exists()) {
            try {
                FileUtils.forceMkdirParent(file);
                file.createNewFile();
            } catch (IOException ex) {
                LOG.error("createNewFile.IOException", ex);
            }
        }
        try ( FileOutputStream fos = new FileOutputStream(file)) {
            mapper.enable(SerializationFeature.INDENT_OUTPUT);
            mapper.writeValue(fos, mapReq);
        } catch (IOException ex) {
            LOG.error("writeValue.IOException", ex);
        }
    }

    @FXML
    void newReq(ActionEvent event) {
        LOG.info("newReq:{}", event);
        urlTxt.setText("");
        jsonParamTxa.setText("");
    }

    @FXML
    void openSelectFiles(ActionEvent event) {
        mapFile = new HashMap<>();
        fileList.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
        FileChooser fc = new FileChooser();
        fc.setTitle("Select Files");
        Node node = (Node) event.getSource();
        List<File> files = fc.showOpenMultipleDialog(node.getScene().getWindow());
        if (files == null) {
            return;
        }
        ObservableList<String> data = FXCollections.observableArrayList();
        files.forEach(file -> {
            data.addAll(file.getName());
            mapFile.put(file.getName(), file);
        });
        fileList.setItems(data);
    }

    @FXML
    void delSelectedFile(KeyEvent event) {
        delSelected(event, fileList, mapFile);
    }

    @FXML
    void sendRequest(ActionEvent event) {
        httpReq = createHttpReq();
        configFormReq(httpReq);
        configUpFileReq(httpReq);
        boolean validReq = validateRequest(httpReq);
        if (validReq) {
            CompletableFuture.runAsync(() -> execRequset(httpReq));
        }
    }

    @FXML
    void commitHeader(TableColumn.CellEditEvent<Parameter, String> event) {
        updateRowData(event);
        addRow(headerReq, event);
    }

    @FXML
    void commitParam(TableColumn.CellEditEvent<Parameter, String> event) {
        updateRowData(event);
        addRow(formParam, event);
    }

    private void initChBoxValue() {
        methodChB.getItems().addAll(HttpMethod.getValues());
        methodChB.setValue(HttpMethod.GET.value());
        accessTypeChb.getItems().addAll(AccessType.getValues());
        accessTypeChb.setValue(AccessType.APPLICATION_JSON.value());
        charsetChb.getItems().addAll(Charset.getValues());
        charsetChb.setValue(Charset.UTF_8.value());
    }

    private void initHeaderTable() {
        initColumn(colHeaderKey, colHeaderVal, colHeaderDesc, colHeaderDel);
        ObservableList<Parameter> data = FXCollections.observableArrayList();
        data.add(new Parameter("Content-Type", "application/json", ""));
        data.add(new Parameter("", "", ""));
        headerReq.setItems(data);
    }

    private void initParamTable() {
        initColumn(colParamKey, colParamVal, colParamDesc, colParamDel);
        ObservableList<Parameter> data = FXCollections.observableArrayList();
        data.add(new Parameter("", "", ""));
        formParam.setItems(data);
    }

    private void initAndRegEvent() {
        MultipleSelectionModel<String> selectModel = reqList.getSelectionModel();
        selectModel.setSelectionMode(SelectionMode.MULTIPLE);
        selectModel.selectedItemProperty().addListener(cl -> {
            updateReq();
        });
        reqList.addEventHandler(KeyEvent.KEY_RELEASED, ke -> {
            delSelected(ke, reqList, mapReq);
        });
        urlTxt.addEventHandler(KeyEvent.KEY_RELEASED, (ke) -> {
            if (ke.getCode() == KeyCode.ENTER) {
                sendBtn.fireEvent(new ActionEvent());
            }
        });
        copyRespBtn.setOnAction(eh -> {
            ClipboardContent content = new ClipboardContent();
            content.putString(jsonRespTxa.getText().trim());
            Clipboard board = Clipboard.getSystemClipboard();
            board.setContent(content);

            NotifyBox notify = new NotifyBox(copyRespBtn.getScene().getWindow());
            notify.info("Copied to clipboard");
        });
    }

    private void listenerMethod() {
        methodChB.setOnAction(eh -> {
            String value = methodChB.getValue();
            HttpMethod method = HttpMethod.valueOf(value);
            switch (method) {
                case POST:
                    accessTypeChb.setValue(AccessType.APPLICATION_JSON.value());
                    break;
                case MULTIPART:
                    accessTypeChb.setValue(AccessType.MULTIPART_FORM_DATA.value());
                default:
                    break;
            }
        });
    }

    private void listenerAccessType() {
        accessTypeChb.setOnAction(eh -> {
            String value = accessTypeChb.getValue();
            AccessType accessType = AccessType.find(value);
            AccessType[] formTypes = {AccessType.MULTIPART_FORM_DATA, AccessType.APPLICATION_FORM_URLENCODED};
            boolean visibleForm = Arrays.asList(formTypes).contains(accessType);
            formParamPane.setVisible(visibleForm);
            jsonParamTxa.setVisible(!visibleForm);
        });
    }

    private HttpReq createHttpReq() {
        String method = methodChB.getValue();
        String uri = urlTxt.getText().trim();
        String contentType = accessTypeChb.getValue();
        String jsonParam = jsonParamTxa.getText().trim();
        String charset = charsetChb.getValue();
        Map<String, String> header = loadTableData(headerReq);
        HttpReq req = new HttpReq();
        req.setUri(uri);
        req.setContentType(contentType);
        req.setCharset(charset);
        req.setMethod(method);
        req.setJsonParam(jsonParam);
        req.setHeader(header);
        LOG.info("request:{}", req);
        return req;
    }

    private void configUpFileReq(HttpReq httpReq) {
        if (mapFile == null) {
            return;
        }
        String filesKey = filesKeyTxt.getText().trim();
        Collection<File> files = mapFile.values();
        List<File> lstFile = new ArrayList<>(files);
        httpReq.setFilesKey(filesKey);
        httpReq.setLstFile(lstFile);
    }

    private void configFormReq(HttpReq httpReq) {
        Map<String, String> param = loadTableData(formParam);
        httpReq.setFormParam(param);
    }

    private Map<String, String> loadTableData(TableView<Parameter> table) {
        Map<String, String> map = new HashMap<>();
        ObservableList<Parameter> items = table.getItems();
        for (Parameter item : items) {
            boolean anyBlank = StringUtils.isAnyBlank(item.getKey(), item.getVal());
            if (!anyBlank) {
                map.put(item.getKey(), item.getVal());
            }
        }
        return map;
    }

    private boolean validateRequest(HttpReq httpReq) {
        if (StringUtils.isBlank(httpReq.getUri())) {
            return false;
        }
        return true;
    }

    private void execRequset(HttpReq httpReq) {
        try {
            HttpRClientX clientX = new HttpRClientX();
            HttpResp httpResp = clientX.execute(httpReq);
            Platform.runLater(() -> outResp(httpResp));
        } catch (IOException ex) {
            LOG.error("convertCallRequset.IOException", ex);
        }
    }

    private void outResp(HttpResp httpResp) {
        outRespBody(httpResp);
        outRespHeader(httpReq, httpResp);
    }

    private void outRespHeader(HttpReq req, HttpResp resp) {
        try {
            ObjectMapper mapper = new ObjectMapper();
            String reqH = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(req.getHeader());
            String respH = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(resp.getHeader());
            StringBuilder builder = new StringBuilder();
            builder.append("----Request Header----").append(StringUtils.LF)
                    .append(reqH)
                    .append(StringUtils.LF).append(StringUtils.LF)
                    .append("----Response Header----").append(StringUtils.LF)
                    .append(respH);
            headerResp.setText(builder.toString());
        } catch (JsonProcessingException ex) {
            LOG.error("outRespHeader", ex);
        }
    }

    private void outRespBody(HttpResp resp) {
        String body = resp.getBody();
        String pretty = JsonUtil.formatJson(body);
        jsonRespTxa.setText(pretty);
        formatRawBtn.setOnAction(eh -> {
            String text = formatRawBtn.isSelected() ? body : pretty;
            jsonRespTxa.setText(text);
        });
        if (JsonUtil.validJson(body)) {
            TreeItem<String> root = JSONTree.createItem("Body", JsonParser.parseString(body));
            root.setExpanded(true);
            jsonView.setRoot(root);
        }
    }

    private void addRow(TableView<Parameter> table, TableColumn.CellEditEvent<Parameter, String> event) {
        int row = event.getTablePosition().getRow();
        int size = event.getTableView().getItems().size();
        if (size - row == 1) {
            table.getItems().add(new Parameter("", "", ""));
        }
    }

    private void updateRowData(TableColumn.CellEditEvent<Parameter, String> event) {
        Parameter row = event.getRowValue();
        String value = event.getNewValue();
        String id = event.getTableColumn().getId();
        switch (id) {
            case "key":
                row.setKey(value);
                break;
            case "value":
                row.setVal(value);
                break;
            case "desc":
                row.setDesc(value);
            default:
                break;
        }
    }

    private void initColumn(TableColumn<Parameter, String> colKey, TableColumn<Parameter, String> colVal, TableColumn<Parameter, String> colDesc, TableColumn<Parameter, String> colDel) {
        TableView<Parameter> table = colKey.getTableView();
        colKey.setCellValueFactory(new PropertyValueFactory<>("key"));
        colVal.setCellValueFactory(new PropertyValueFactory<>("val"));
        colDesc.setCellValueFactory(new PropertyValueFactory<>("desc"));
        colKey.setCellFactory(TextFieldTableCell.forTableColumn());
        colVal.setCellFactory(TextFieldTableCell.forTableColumn());
        colDesc.setCellFactory(TextFieldTableCell.forTableColumn());
        colKey.prefWidthProperty().bind(table.widthProperty().multiply(.2));
        colVal.prefWidthProperty().bind(table.widthProperty().multiply(.3));
        colDesc.prefWidthProperty().bind(table.widthProperty().multiply(.4));
        colDel.prefWidthProperty().bind(table.widthProperty().multiply(.1));
        initDelColumn(colDel);
    }

    private void initDelColumn(TableColumn<Parameter, String> col) {
        col.setCellFactory(clbck -> {
            TableCell<Parameter, String> cell = new TableCell<Parameter, String>() {
                @Override
                protected void updateItem(String item, boolean empty) {
                    super.updateItem(item, empty);
                    Button del = new Button("X");
                    del.setOnAction(event -> {
                        ObservableList<Parameter> items = this.getTableView().getItems();
                        int index = this.getIndex();
                        int size = items.size();
                        if (size == 1) {
                            items.clear();
                            items.add(new Parameter("", "", ""));
                        } else if (index < size) {
                            items.remove(index);
                        }
                    });
                    if (empty) {
                        this.setText(null);
                        this.setGraphic(null);
                    } else {
                        this.setGraphic(del);
                    }
                }
            };
            return cell;
        });
    }

    private void delSelected(KeyEvent event, ListView<String> list, Map<String, ?> map) {
        KeyCode code = event.getCode();
        boolean delete = code == KeyCode.BACK_SPACE || code == KeyCode.DELETE;
        if (!delete) {
            return;
        }
        ObservableList<String> selItems = list.getSelectionModel().getSelectedItems();
        list.getItems().removeAll(selItems);
        Stream.iterate(0, i -> i + 1).limit(selItems.size()).forEach(i -> {
            map.remove(selItems.get(i));
        });
    }

    private void loadHistory() {
        String path = JavaUtil.loadUrl("").getPath();
        File file = FileUtils.getFile(path + HIS_FILE);
        if (!file.exists()) {
            return;
        }
        ObjectMapper mapper = new JsonMapper();
        TypeReference<Map<String, HttpReq>> mapRef = new TypeReference<>() {
        };
        ObservableList<String> items = reqList.getItems();
        try {
            mapReq = mapper.readValue(file, mapRef);
            mapReq.forEach((key, value) -> {
                items.add(key);
            });
        } catch (IOException ex) {
            LOG.error("loadHistory.IOException", ex);
        }
    }

    private void updateReq() {
        String uri = reqList.getSelectionModel().getSelectedItem();
        HttpReq req = mapReq.get(uri);
        urlTxt.setText(uri);
        methodChB.setValue(req.getMethod());
        jsonParamTxa.setText(req.getJsonParam());
        accessTypeChb.setValue(req.getContentType());
    }

}
